//
//  tun_win32.c
//  myvpn
//
//  Created by Zhuobin He on 13-1-11.
//  Copyright (c) 2013骞�Zhuobin He. All rights reserved.
//

#include "SysLib.h"

#ifdef OS_WIN

#include "Common.h"
#include "tap-windows.h"
#include "TunDev.h"

struct _TapReg {
    char *name;
    char *guid;
    char *deviceInstanceId;
    struct _TapReg *next;
};

typedef struct _TapReg TapReg;

void tapreg_delete(TapReg *taplist) {
    for (;taplist;) {
        free(taplist->guid);
        free(taplist->deviceInstanceId);
        free(taplist->name);
        TapReg *tmp = taplist->next;
        free(taplist);
        taplist = tmp;
    }
}

TapReg * get_tap_reg ()
{
    HKEY adapter_key;
    LONG status;
    DWORD len;
    TapReg *first = NULL;
    TapReg *last = NULL;
    int i = 0;
    
    status = RegOpenKeyEx(HKEY_LOCAL_MACHINE, ADAPTER_KEY, 0, KEY_READ, &adapter_key);
    
    if (status != ERROR_SUCCESS) {
        log_error("Error opening registry key");
        return NULL;
    }
    while (true)
    {
        char enum_name[256];
        char unit_string[256];
        HKEY unit_key;
        char component_id_string[] = "ComponentId";
        char component_id[256];
        char net_cfg_instance_id_string[] = "NetCfgInstanceId";
        char device_instance_id_key[] = "PnpInstanceID";
        char net_cfg_instance_id[256];
        char device_instance_id[256];
        DWORD data_type;
        
        len = sizeof (enum_name);
        status = RegEnumKeyEx(adapter_key, i, enum_name, &len, NULL, NULL, NULL, NULL);
        if (status == ERROR_NO_MORE_ITEMS) {
            break;
        } else if (status != ERROR_SUCCESS) {
            log_debug("Error enumerating registry subkeys of key");
            continue;
        }
        _snprintf (unit_string, sizeof(unit_string), "%s\\%s", ADAPTER_KEY, enum_name);
        
        status = RegOpenKeyEx(HKEY_LOCAL_MACHINE, unit_string, 0, KEY_READ, &unit_key);
        
        if (status != ERROR_SUCCESS) {
            log_debug("Error opening registry key: %s", unit_string);
        } else {
            len = sizeof (component_id);
            status = RegQueryValueEx(unit_key, component_id_string, NULL, &data_type, (BYTE *)component_id, &len);
            if (status != ERROR_SUCCESS || data_type != REG_SZ) {
                log_debug("Error opening registry key: %s\\%s", unit_string, component_id_string);
            } else {
                component_id[len] = '\0';
                len = sizeof (net_cfg_instance_id);
                status = RegQueryValueEx(unit_key, net_cfg_instance_id_string, NULL, &data_type, (BYTE *)net_cfg_instance_id, &len);

                if (status == ERROR_SUCCESS && data_type == REG_SZ)
                {
                    net_cfg_instance_id[len] = '\0';
                    if ((!strcmp (component_id, TAP_COMPONENT_ID)) || (!strcmp (component_id, TAP_COMPONENT_ID2)))
                    {
                        char conn_key_id[1024];
                        HKEY conn_key;
                        snprintf (conn_key_id, sizeof(conn_key_id), "%s\\%s\\Connection", NETWORK_CONNECTIONS_KEY, net_cfg_instance_id);
                        status = RegOpenKeyEx(HKEY_LOCAL_MACHINE, conn_key_id, 0, KEY_READ, &conn_key);
                        if (status != ERROR_SUCCESS) {
                            log_debug("Error opening network reg: %s", conn_key_id);
                        } else {
                            char name_string[] = "Name";
                            char name_value[2048];
                            len = sizeof (name_value);
                            status = RegQueryValueEx(conn_key, name_string, NULL, &data_type, (BYTE *)name_value, &len);
                            if (status == ERROR_SUCCESS && data_type == REG_SZ) {
                                name_value[len] = '\0';
                                len = sizeof (device_instance_id);
                                status = RegQueryValueEx(conn_key, device_instance_id_key, NULL, &data_type, (BYTE *)device_instance_id, &len);
                            }
                            if (status == ERROR_SUCCESS && data_type == REG_SZ) {
                                device_instance_id[len] = '\0';
                                if (!first) {
                                    zmalloc(&first, sizeof(TapReg));
                                    last = first;
                                } else {
                                    zmalloc(&last->next, sizeof(TapReg));
                                    last = last->next;
                                }
                                last->name = string_alloc (name_value);
                                last->guid = string_alloc (net_cfg_instance_id);
                                last->deviceInstanceId = string_alloc (device_instance_id);
                            } else {
                                log_debug("Error query network interface name: %s", unit_string);
                            }
                        }
                        RegCloseKey (conn_key);
                    }
                } else {
                    log_debug("Error query cfg_id for: %s", unit_string);
                }
            }
            RegCloseKey (unit_key);
        }
        ++i;
    }
    
    RegCloseKey (adapter_key);
    return first;
}



int reset_tun_dev(TapReg *tap_reg) {
    HDEVINFO hDevInfo = INVALID_HANDLE_VALUE;
    hDevInfo = SetupDiGetClassDevs(NULL,NULL,NULL, DIGCF_PRESENT | DIGCF_ALLCLASSES);
    if(INVALID_HANDLE_VALUE == hDevInfo)
        return GetLastError();

    SP_DEVINFO_DATA DeviceInfoData = { sizeof(SP_DEVINFO_DATA) };

    char devName[128];
    char instanceId[128];
    int i;
    for (i = 0; SetupDiEnumDeviceInfo(hDevInfo, i, &DeviceInfoData); i++) {
        SetupDiClassNameFromGuid(&DeviceInfoData.ClassGuid, devName, 128, NULL);
        if (!strcmp(devName, "Net")) {
            SetupDiGetDeviceInstanceId(hDevInfo, &DeviceInfoData, instanceId, 128, NULL);
            if (!strncmp(instanceId, tap_reg->deviceInstanceId, strlen(tap_reg->deviceInstanceId))) {
                SP_PROPCHANGE_PARAMS params = { sizeof(SP_CLASSINSTALL_HEADER) };
                params.ClassInstallHeader.InstallFunction = DIF_PROPERTYCHANGE;
                params.Scope = DICS_FLAG_CONFIGSPECIFIC;
                params.StateChange = DICS_DISABLE;
                params.HwProfile = 0;
                SetupDiSetClassInstallParams(hDevInfo, &DeviceInfoData,
                        (SP_CLASSINSTALL_HEADER*) &params,
                        sizeof(SP_PROPCHANGE_PARAMS));
                SetupDiChangeState(hDevInfo, &DeviceInfoData);
                params.StateChange = DICS_ENABLE;
                SetupDiSetClassInstallParams(hDevInfo, &DeviceInfoData,
                        (SP_CLASSINSTALL_HEADER*) &params,
                        sizeof(SP_PROPCHANGE_PARAMS));
                SetupDiChangeState(hDevInfo, &DeviceInfoData);
            }
        }

    }

    SetupDiDestroyDeviceInfoList(hDevInfo);
    return 0;

}

PTunDev tundev_alloc(const char *dev_name) {
//    if (ctrl_tun_dev_status(dev_name, false)
//        || ctrl_tun_dev_status(dev_name, true)) {
//        log_error("Error reset tun dev");
//        return NULL;
//    }
    TapReg *devlist = get_tap_reg();
    if (dev_name != NULL && strlen(dev_name)) {
        for (;devlist;) {
            if (strcmp(devlist->name, dev_name) == 0) {
                break;
            }
            TapReg *tmp = devlist->next;
            devlist->next = NULL;
            tapreg_delete(devlist);
            devlist = tmp;
        }
    }
    if (!devlist) return NULL;
    reset_tun_dev(devlist);
    char ifnum[20];
    {
        char *route_print_rst = (char *) malloc (sizeof(char) * 2048);
        if (execute_cmd("route print", route_print_rst, 2048)) {
            log_error("Error getting ifnum in route print");
            tapreg_delete(devlist);
            return NULL;
        }

        char mac[20];
        int i;
        for (i=0; i<4; i++) {
            strncpy(mac + (i * 3) + 6, devlist->guid + (i * 2) + 1, 2);
            mac[i * 3 + 8] = ' ';
        }
        strncpy(mac, "00 ff ", 6);
        mac[17] = '\0';
        for (i=strlen(mac) - 1; i>=0; i--) mac[i] = tolower(mac[i]);
        for (i=strlen(route_print_rst) - 1; i>=0; i--) route_print_rst[i] = tolower(route_print_rst[i]);

        char *pifnum = strstr(route_print_rst, mac);
        char *pifnum_end = NULL;
        if (!pifnum) {
            log_error("no matching mac interface");
            free(route_print_rst);
            tapreg_delete(devlist);
            return NULL;
        }
        pifnum --;
        for (;pifnum > route_print_rst; pifnum--) {
            char tmp = *pifnum;
            if (tmp >= '0' && tmp <= '9' && pifnum_end == NULL) {
                pifnum_end = pifnum + 1;
            }
            if (tmp == '\r' || tmp == '\n') break;
        }
        int len = pifnum_end - pifnum;
        if (len >= 20) {
            log_error("ifnum from route print too long: %d", len);
            free(route_print_rst);
            tapreg_delete(devlist);
            return NULL;
        }
        strncpy(ifnum, pifnum, len);
        ifnum[len] = '\0';
        trim(ifnum);
        free(route_print_rst);
    }
//    char tmp[256];
//    snprintf (tmp, sizeof(tmp), "route delete if %s", ifnum);
    char device_path[256];
    DWORD len;
    snprintf (device_path, sizeof(device_path), "%s%s%s",
              USERMODEDEVICEDIR,
              devlist->guid,
              TAP_WIN_SUFFIX);
    tapreg_delete(devlist);
    HANDLE hand = CreateFile (
                       device_path,
                       GENERIC_READ | GENERIC_WRITE,
                       0, /* was: FILE_SHARE_READ */
                       0,
                       OPEN_EXISTING,
                       FILE_ATTRIBUTE_SYSTEM | FILE_FLAG_OVERLAPPED,
                       0
                       );
    if (hand == INVALID_HANDLE_VALUE) {
        log_error("Error createfile from guid");
        return NULL;
    }

    {
        ULONG status = TRUE;
        if (!DeviceIoControl (hand, TAP_WIN_IOCTL_SET_MEDIA_STATUS,
                              &status, sizeof (status),
                              &status, sizeof (status), &len, NULL)) {
            log_error("The TAP-Win32 driver rejected a TAP_IOCTL_SET_MEDIA_STATUS DeviceIoControl call.");
            return NULL;
        }
    }
    
    PTunDev ret = NULL;
    zmalloc(&ret, sizeof(TunDev));
    zmalloc(&(ret->olRead), sizeof(OVERLAPPED));
    zmalloc(&(ret->olWrite), sizeof(OVERLAPPED));
    ret->olRead->hEvent = CreateEvent(NULL,TRUE, FALSE, NULL);
    ret->olWrite->hEvent = CreateEvent(NULL,TRUE, FALSE, NULL);
    ret->reading = false;
    ret->handle = hand;
    ret->dev = string_alloc(dev_name);
    ret->ifnum = string_alloc(ifnum);
    return ret;
}


void tundev_delete(TunDev *obj) {
    free(obj->olRead);
    free(obj->olWrite);
    free(obj->ifnum);
    free(obj->dev);
    if (obj->local_ip) free(obj->local_ip);
    if (obj->remote_ip) free(obj->remote_ip);
    free(obj);
}

unsigned int parse_addr(const char *ip_str)
{
    const char *p0 = ip_str;
    const char *p1;
    int i = 0;
    unsigned int ti;
    unsigned int ret = 0;
    char tmp[10];
    for (i=0; i<32; i+=8) {
        p1 = strchr(p0, '.');
        if (p1 == NULL) {
            p1 = strlen(p0) + p0;
        }
        strncpy(tmp, p0, p1-p0);
        tmp[p1-p0] = '\0';
        sscanf(tmp, "%ud", &ti);
        ret |= ti << i;
        p0 = p1 + 1;
    }
    return ret;
}

int tundev_ifconfig(PTunDev tun, const char *local_ip, const char *remote_ip)
{
    unsigned int ep[3];
    BOOL status;
    DWORD len;
    ep[0] = parse_addr(local_ip);
    //ep[1] = parse_addr(remote_ip);
    ep[2] = 0x00ffffff;
    ep[1] = ep[0] & ep[2];

    status = DeviceIoControl (tun->handle, TAP_WIN_IOCTL_CONFIG_TUN,
                                ep, sizeof (ep),
                                ep, sizeof (ep), &len, NULL);
    char tmp[256];
    sprintf(tmp, "netsh interface ip set address \"%s\" static %s 255.255.255.0 %s 1000", tun->dev, local_ip, remote_ip);
    tun->local_ip = string_alloc(local_ip);
    tun->remote_ip = string_alloc(remote_ip);
    return system(tmp);
}


int tundev_read(PTunDev tun, char *buffer, int limit) {
    DWORD nread;
    
    BOOL ret = ReadFile(tun->handle, buffer, limit, &nread, NULL);
    
    if (!ret) {
        int error_code = GetLastError();
        log_error("Read error code: %ul", error_code);
        return -1;
    }
    return (int)nread;
}

int tundev_write(PTunDev tun, const char *data, int size) {
    if (!size) return 0;
    DWORD nwrite;
    HANDLE hEvents[] = {tun->olWrite->hEvent};
    BOOL ret = WriteFile(tun->handle, data, size, &nwrite, tun->olWrite);
    if (!ret) {
        int error = GetLastError();
        if (error != ERROR_IO_PENDING) {
            log_error("Write to tun ret false, errorno: %d", error);
            return -1;
        }
    }
    DWORD dwWaitRet = WaitForMultipleObjects(1, hEvents, TRUE, 5000); //Wait multiple IO requests  
    if (dwWaitRet - WAIT_OBJECT_0 != 0) {
        log_error("Write to tun return %d while wait", dwWaitRet);
        return -1;
    } else {
        if (tun->olWrite->Internal != ERROR_SUCCESS) {
            log_error("Async write to tun notify error: %d", tun->olWrite->Internal);
            return -1;
        }
        if (tun->olWrite->InternalHigh != size) {
            log_error("Async write to tun ret size: %d != %d", tun->olWrite->InternalHigh, size);
            return -1;
        }
    }
    return nwrite;
}

void tundev_close(PTunDev tun) {
    log_info("Closing tun dev");
    if (!CloseHandle(tun->handle)) {
        log_error("Error closing tun handle, error: %d", GetLastError());
    }
    CloseHandle(tun->olRead->hEvent);
    CloseHandle(tun->olWrite->hEvent);
}

int tundev_route(PTunDev dev, const char *address, int prefix) {
    char cmdstr[256];
    unsigned int mask_int = 0;
    char mask_str[20] = "";
    int i;
    for (i=0; i<prefix; i++) {
        mask_int |= 1u << (31 - i);
    }
    sprintf(mask_str, "%d.%d.%d.%d",
            (mask_int >> 24) & 0xff, (mask_int >> 16) & 0xff,
            (mask_int >> 8) & 0xff, mask_int & 0xff);

    sprintf(cmdstr, "route ADD %s MASK %s %s IF %s", address, mask_str, dev->remote_ip, dev->ifnum);
    log_info("cmd: %s", cmdstr);
    return system(cmdstr);
}

#endif
